/*-------------------------------------------------------------------------
 *
 * be-secure-openssl.c
 *      functions for OpenSSL support in the backend.
 *
 *
 * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *      src/backend/libpq/be-secure-openssl.c
 *
 *      Since the server static private key ($DataDir/server.key)
 *      will normally be stored unencrypted so that the database
 *      backend can restart automatically, it is important that
 *      we select an algorithm that continues to provide confidentiality
 *      even if the attacker has the server's private key.  Ephemeral
 *      DH (EDH) keys provide this and more (Perfect Forward Secrecy
 *      aka PFS).
 *
 *      N.B., the static private key should still be protected to
 *      the largest extent possible, to minimize the risk of
 *      impersonations.
 *
 *      Another benefit of EDH is that it allows the backend and
 *      clients to use DSA keys.  DSA keys can only provide digital
 *      signatures, not encryption, and are often acceptable in
 *      jurisdictions where RSA keys are unacceptable.
 *
 *      The downside to EDH is that it makes it impossible to
 *      use ssldump(1) if there's a problem establishing an SSL
 *      session.  In this case you'll need to temporarily disable
 *      EDH by commenting out the callback.
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <sys/stat.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netdb.h>
#include <netinet/in.h>
#ifdef HAVE_NETINET_TCP_H
#include <netinet/tcp.h>
#include <arpa/inet.h>
#endif

#include <openssl/ssl.h>
#include <openssl/dh.h>
#include <openssl/conf.h>
#ifndef OPENSSL_NO_ECDH
#include <openssl/ec.h>
#endif

#include "libpq/libpq.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/fd.h"
#include "storage/latch.h"
#include "tcop/tcopprot.h"
#include "utils/memutils.h"


static int    my_sock_read(BIO *h, char *buf, int size);
static int    my_sock_write(BIO *h, const char *buf, int size);
static BIO_METHOD *my_BIO_s_socket(void);
static int    my_SSL_set_fd(Port *port, int fd);

static DH  *load_dh_file(char *filename, bool isServerStart);
static DH  *load_dh_buffer(const char *, size_t);
static int    ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata);
static int    verify_cb(int, X509_STORE_CTX *);
static void info_cb(const SSL *ssl, int type, int args);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessage(unsigned long ecode);

static char *X509_NAME_to_cstring(X509_NAME *name);

static SSL_CTX *SSL_context = NULL;
static bool SSL_initialized = false;
static bool ssl_passwd_cb_called = false;

/* ------------------------------------------------------------ */
/*                         Hardcoded values                        */
/* ------------------------------------------------------------ */

/*
 *    Hardcoded DH parameters, used in ephemeral DH keying.
 *    As discussed above, EDH protects the confidentiality of
 *    sessions even if the static private key is compromised,
 *    so we are *highly* motivated to ensure that we can use
 *    EDH even if the DBA has not provided custom DH parameters.
 *
 *    We could refuse SSL connections unless a good DH parameter
 *    file exists, but some clients may quietly renegotiate an
 *    unsecured connection without fully informing the user.
 *    Very uncool. Alternatively, the system could refuse to start
 *    if a DH parameters is not specified, but this would tend to
 *    piss off DBAs.
 *
 *    If you want to create your own hardcoded DH parameters
 *    for fun and profit, review "Assigned Number for SKIP
 *    Protocols" (http://www.skip-vpn.org/spec/numbers.html)
 *    for suggestions.
 */

static const char file_dh2048[] =
"-----BEGIN DH PARAMETERS-----\n\
MIIBCAKCAQEA9kJXtwh/CBdyorrWqULzBej5UxE5T7bxbrlLOCDaAadWoxTpj0BV\n\
89AHxstDqZSt90xkhkn4DIO9ZekX1KHTUPj1WV/cdlJPPT2N286Z4VeSWc39uK50\n\
T8X8dryDxUcwYc58yWb/Ffm7/ZFexwGq01uejaClcjrUGvC/RgBYK+X0iP1YTknb\n\
zSC0neSRBzZrM2w4DUUdD3yIsxx8Wy2O9vPJI8BD8KVbGI2Ou1WMuF040zT9fBdX\n\
Q6MdGGzeMyEstSr/POGxKUAYEY18hKcKctaGxAMZyAcpesqVDNmWn6vQClCbAkbT\n\
CD1mpF1Bn5x8vYlLIhkmuquiXsNV6TILOwIBAg==\n\
-----END DH PARAMETERS-----\n";


/* ------------------------------------------------------------ */
/*                         Public interface                        */
/* ------------------------------------------------------------ */

/*
 *    Initialize global SSL context.
 *
 * If isServerStart is true, report any errors as FATAL (so we don't return).
 * Otherwise, log errors at LOG level and return -1 to indicate trouble,
 * preserving the old SSL state if any.  Returns 0 if OK.
 */
int
be_tls_init(bool isServerStart)
{// #lizard forgives
    STACK_OF(X509_NAME) *root_cert_list = NULL;
    SSL_CTX    *context;
    struct stat buf;

    /* This stuff need be done only once. */
    if (!SSL_initialized)
    {
#ifdef HAVE_OPENSSL_INIT_SSL
        OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, NULL);
#else
        OPENSSL_config(NULL);
        SSL_library_init();
        SSL_load_error_strings();
#endif
        SSL_initialized = true;
    }

    /*
     * We use SSLv23_method() because it can negotiate use of the highest
     * mutually supported protocol version, while alternatives like
     * TLSv1_2_method() permit only one specific version.  Note that we don't
     * actually allow SSL v2 or v3, only TLS protocols (see below).
     */
    context = SSL_CTX_new(SSLv23_method());
    if (!context)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errmsg("could not create SSL context: %s",
                        SSLerrmessage(ERR_get_error()))));
        goto error;
    }

    /*
     * Disable OpenSSL's moving-write-buffer sanity check, because it causes
     * unnecessary failures in nonblocking send cases.
     */
    SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);

    /*
     * If reloading, override OpenSSL's default handling of
     * passphrase-protected files, because we don't want to prompt for a
     * passphrase in an already-running server.  (Not that the default
     * handling is very desirable during server start either, but some people
     * insist we need to keep it.)
     */
    if (!isServerStart)
        SSL_CTX_set_default_passwd_cb(context, ssl_passwd_cb);

    /*
     * Load and verify server's certificate and private key
     */
    if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("could not load server certificate file \"%s\": %s",
                        ssl_cert_file, SSLerrmessage(ERR_get_error()))));
        goto error;
    }

    if (stat(ssl_key_file, &buf) != 0)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode_for_file_access(),
                 errmsg("could not access private key file \"%s\": %m",
                        ssl_key_file)));
        goto error;
    }

    if (!S_ISREG(buf.st_mode))
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("private key file \"%s\" is not a regular file",
                        ssl_key_file)));
        goto error;
    }

    /*
     * Refuse to load key files owned by users other than us or root.
     *
     * XXX surely we can check this on Windows somehow, too.
     */
#if !defined(WIN32) && !defined(__CYGWIN__)
    if (buf.st_uid != geteuid() && buf.st_uid != 0)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("private key file \"%s\" must be owned by the database user or root",
                        ssl_key_file)));
        goto error;
    }
#endif

    /*
     * Require no public access to key file. If the file is owned by us,
     * require mode 0600 or less. If owned by root, require 0640 or less to
     * allow read access through our gid, or a supplementary gid that allows
     * to read system-wide certificates.
     *
     * XXX temporarily suppress check when on Windows, because there may not
     * be proper support for Unix-y file permissions.  Need to think of a
     * reasonable check to apply on Windows.  (See also the data directory
     * permission check in postmaster.c)
     */
#if !defined(WIN32) && !defined(__CYGWIN__)
    if ((buf.st_uid == geteuid() && buf.st_mode & (S_IRWXG | S_IRWXO)) ||
        (buf.st_uid == 0 && buf.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)))
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("private key file \"%s\" has group or world access",
                        ssl_key_file),
                 errdetail("File must have permissions u=rw (0600) or less if owned by the database user, or permissions u=rw,g=r (0640) or less if owned by root.")));
        goto error;
    }
#endif

    /*
     * OK, try to load the private key file.
     */
    ssl_passwd_cb_called = false;

    if (SSL_CTX_use_PrivateKey_file(context,
                                    ssl_key_file,
                                    SSL_FILETYPE_PEM) != 1)
    {
        if (ssl_passwd_cb_called)
            ereport(isServerStart ? FATAL : LOG,
                    (errcode(ERRCODE_CONFIG_FILE_ERROR),
                     errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
                            ssl_key_file)));
        else
            ereport(isServerStart ? FATAL : LOG,
                    (errcode(ERRCODE_CONFIG_FILE_ERROR),
                     errmsg("could not load private key file \"%s\": %s",
                            ssl_key_file, SSLerrmessage(ERR_get_error()))));
        goto error;
    }

    if (SSL_CTX_check_private_key(context) != 1)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("check of private key failed: %s",
                        SSLerrmessage(ERR_get_error()))));
        goto error;
    }

    /* disallow SSL v2/v3 */
    SSL_CTX_set_options(context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);

    /* disallow SSL session tickets */
#ifdef SSL_OP_NO_TICKET            /* added in openssl 0.9.8f */
    SSL_CTX_set_options(context, SSL_OP_NO_TICKET);
#endif

    /* disallow SSL session caching, too */
    SSL_CTX_set_session_cache_mode(context, SSL_SESS_CACHE_OFF);

    /* set up ephemeral DH and ECDH keys */
    if (!initialize_dh(context, isServerStart))
        goto error;
    if (!initialize_ecdh(context, isServerStart))
        goto error;

    /* set up the allowed cipher list */
    if (SSL_CTX_set_cipher_list(context, SSLCipherSuites) != 1)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("could not set the cipher list (no valid ciphers available)")));
        goto error;
    }

    /* Let server choose order */
    if (SSLPreferServerCiphers)
        SSL_CTX_set_options(context, SSL_OP_CIPHER_SERVER_PREFERENCE);

    /*
     * Load CA store, so we can verify client certificates if needed.
     */
    if (ssl_ca_file[0])
    {
        if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
            (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
        {
            ereport(isServerStart ? FATAL : LOG,
                    (errcode(ERRCODE_CONFIG_FILE_ERROR),
                     errmsg("could not load root certificate file \"%s\": %s",
                            ssl_ca_file, SSLerrmessage(ERR_get_error()))));
            goto error;
        }
    }

    /*----------
     * Load the Certificate Revocation List (CRL).
     * http://searchsecurity.techtarget.com/sDefinition/0,,sid14_gci803160,00.html
     *----------
     */
    if (ssl_crl_file[0])
    {
        X509_STORE *cvstore = SSL_CTX_get_cert_store(context);

        if (cvstore)
        {
            /* Set the flags to check against the complete CRL chain */
            if (X509_STORE_load_locations(cvstore, ssl_crl_file, NULL) == 1)
            {
                /* OpenSSL 0.96 does not support X509_V_FLAG_CRL_CHECK */
#ifdef X509_V_FLAG_CRL_CHECK
                X509_STORE_set_flags(cvstore,
                                     X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL);
#else
                ereport(LOG,
                        (errcode(ERRCODE_CONFIG_FILE_ERROR),
                         errmsg("SSL certificate revocation list file \"%s\" ignored",
                                ssl_crl_file),
                         errdetail("SSL library does not support certificate revocation lists.")));
#endif
            }
            else
            {
                ereport(isServerStart ? FATAL : LOG,
                        (errcode(ERRCODE_CONFIG_FILE_ERROR),
                         errmsg("could not load SSL certificate revocation list file \"%s\": %s",
                                ssl_crl_file, SSLerrmessage(ERR_get_error()))));
                goto error;
            }
        }
    }

    if (ssl_ca_file[0])
    {
        /*
         * Always ask for SSL client cert, but don't fail if it's not
         * presented.  We might fail such connections later, depending on what
         * we find in pg_hba.conf.
         */
        SSL_CTX_set_verify(context,
                           (SSL_VERIFY_PEER |
                            SSL_VERIFY_CLIENT_ONCE),
                           verify_cb);

        /*
         * Tell OpenSSL to send the list of root certs we trust to clients in
         * CertificateRequests.  This lets a client with a keystore select the
         * appropriate client certificate to send to us.
         */
        SSL_CTX_set_client_CA_list(context, root_cert_list);
    }

    /*
     * Success!  Replace any existing SSL_context.
     */
    if (SSL_context)
        SSL_CTX_free(SSL_context);

    SSL_context = context;

    /*
     * Set flag to remember whether CA store has been loaded into SSL_context.
     */
    if (ssl_ca_file[0])
        ssl_loaded_verify_locations = true;
    else
        ssl_loaded_verify_locations = false;

    return 0;

error:
    if (context)
        SSL_CTX_free(context);
    return -1;
}

/*
 *    Destroy global SSL context, if any.
 */
void
be_tls_destroy(void)
{
    if (SSL_context)
        SSL_CTX_free(SSL_context);
    SSL_context = NULL;
    ssl_loaded_verify_locations = false;
}

/*
 *    Attempt to negotiate SSL connection.
 */
int
be_tls_open_server(Port *port)
{// #lizard forgives
    int            r;
    int            err;
    int            waitfor;
    unsigned long ecode;

    Assert(!port->ssl);
    Assert(!port->peer);

    if (!SSL_context)
    {
        ereport(COMMERROR,
                (errcode(ERRCODE_PROTOCOL_VIOLATION),
                 errmsg("could not initialize SSL connection: SSL context not set up")));
        return -1;
    }

    if (!(port->ssl = SSL_new(SSL_context)))
    {
        ereport(COMMERROR,
                (errcode(ERRCODE_PROTOCOL_VIOLATION),
                 errmsg("could not initialize SSL connection: %s",
                        SSLerrmessage(ERR_get_error()))));
        return -1;
    }
    if (!my_SSL_set_fd(port, port->sock))
    {
        ereport(COMMERROR,
                (errcode(ERRCODE_PROTOCOL_VIOLATION),
                 errmsg("could not set SSL socket: %s",
                        SSLerrmessage(ERR_get_error()))));
        return -1;
    }
    port->ssl_in_use = true;

aloop:

    /*
     * Prepare to call SSL_get_error() by clearing thread's OpenSSL error
     * queue.  In general, the current thread's error queue must be empty
     * before the TLS/SSL I/O operation is attempted, or SSL_get_error() will
     * not work reliably.  An extension may have failed to clear the
     * per-thread error queue following another call to an OpenSSL I/O
     * routine.
     */
    ERR_clear_error();
    r = SSL_accept(port->ssl);
    if (r <= 0)
    {
        err = SSL_get_error(port->ssl, r);

        /*
         * Other clients of OpenSSL in the backend may fail to call
         * ERR_get_error(), but we always do, so as to not cause problems for
         * OpenSSL clients that don't call ERR_clear_error() defensively.  Be
         * sure that this happens by calling now. SSL_get_error() relies on
         * the OpenSSL per-thread error queue being intact, so this is the
         * earliest possible point ERR_get_error() may be called.
         */
        ecode = ERR_get_error();
        switch (err)
        {
            case SSL_ERROR_WANT_READ:
            case SSL_ERROR_WANT_WRITE:
                /* not allowed during connection establishment */
                Assert(!port->noblock);

                /*
                 * No need to care about timeouts/interrupts here. At this
                 * point authentication_timeout still employs
                 * StartupPacketTimeoutHandler() which directly exits.
                 */
                if (err == SSL_ERROR_WANT_READ)
                    waitfor = WL_SOCKET_READABLE;
                else
                    waitfor = WL_SOCKET_WRITEABLE;

                WaitLatchOrSocket(MyLatch, waitfor, port->sock, 0,
                                  WAIT_EVENT_SSL_OPEN_SERVER);
                goto aloop;
            case SSL_ERROR_SYSCALL:
                if (r < 0)
                    ereport(COMMERROR,
                            (errcode_for_socket_access(),
                             errmsg("could not accept SSL connection: %m")));
                else
                    ereport(COMMERROR,
                            (errcode(ERRCODE_PROTOCOL_VIOLATION),
                             errmsg("could not accept SSL connection: EOF detected")));
                break;
            case SSL_ERROR_SSL:
                ereport(COMMERROR,
                        (errcode(ERRCODE_PROTOCOL_VIOLATION),
                         errmsg("could not accept SSL connection: %s",
                                SSLerrmessage(ecode))));
                break;
            case SSL_ERROR_ZERO_RETURN:
                ereport(COMMERROR,
                        (errcode(ERRCODE_PROTOCOL_VIOLATION),
                         errmsg("could not accept SSL connection: EOF detected")));
                break;
            default:
                ereport(COMMERROR,
                        (errcode(ERRCODE_PROTOCOL_VIOLATION),
                         errmsg("unrecognized SSL error code: %d",
                                err)));
                break;
        }
        return -1;
    }

    /* Get client certificate, if available. */
    port->peer = SSL_get_peer_certificate(port->ssl);

    /* and extract the Common Name from it. */
    port->peer_cn = NULL;
    port->peer_cert_valid = false;
    if (port->peer != NULL)
    {
        int            len;

        len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
                                        NID_commonName, NULL, 0);
        if (len != -1)
        {
            char       *peer_cn;

            peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
            r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
                                          NID_commonName, peer_cn, len + 1);
            peer_cn[len] = '\0';
            if (r != len)
            {
                /* shouldn't happen */
                pfree(peer_cn);
                return -1;
            }

            /*
             * Reject embedded NULLs in certificate common name to prevent
             * attacks like CVE-2009-4034.
             */
            if (len != strlen(peer_cn))
            {
                ereport(COMMERROR,
                        (errcode(ERRCODE_PROTOCOL_VIOLATION),
                         errmsg("SSL certificate's common name contains embedded null")));
                pfree(peer_cn);
                return -1;
            }

            port->peer_cn = peer_cn;
        }
        port->peer_cert_valid = true;
    }

    ereport(DEBUG2,
            (errmsg("SSL connection from \"%s\"",
                    port->peer_cn ? port->peer_cn : "(anonymous)")));

    /* set up debugging/info callback */
    SSL_CTX_set_info_callback(SSL_context, info_cb);

    return 0;
}

/*
 *    Close SSL connection.
 */
void
be_tls_close(Port *port)
{
    if (port->ssl)
    {
        SSL_shutdown(port->ssl);
        SSL_free(port->ssl);
        port->ssl = NULL;
        port->ssl_in_use = false;
    }

    if (port->peer)
    {
        X509_free(port->peer);
        port->peer = NULL;
    }

    if (port->peer_cn)
    {
        pfree(port->peer_cn);
        port->peer_cn = NULL;
    }
}

/*
 *    Read data from a secure connection.
 */
ssize_t
be_tls_read(Port *port, void *ptr, size_t len, int *waitfor)
{// #lizard forgives
    ssize_t        n;
    int            err;
    unsigned long ecode;

    errno = 0;
    ERR_clear_error();
    n = SSL_read(port->ssl, ptr, len);
    err = SSL_get_error(port->ssl, n);
    ecode = (err != SSL_ERROR_NONE || n < 0) ? ERR_get_error() : 0;
    switch (err)
    {
        case SSL_ERROR_NONE:
            /* a-ok */
            break;
        case SSL_ERROR_WANT_READ:
            *waitfor = WL_SOCKET_READABLE;
            errno = EWOULDBLOCK;
            n = -1;
            break;
        case SSL_ERROR_WANT_WRITE:
            *waitfor = WL_SOCKET_WRITEABLE;
            errno = EWOULDBLOCK;
            n = -1;
            break;
        case SSL_ERROR_SYSCALL:
            /* leave it to caller to ereport the value of errno */
            if (n != -1)
            {
                errno = ECONNRESET;
                n = -1;
            }
            break;
        case SSL_ERROR_SSL:
            ereport(COMMERROR,
                    (errcode(ERRCODE_PROTOCOL_VIOLATION),
                     errmsg("SSL error: %s", SSLerrmessage(ecode))));
            errno = ECONNRESET;
            n = -1;
            break;
        case SSL_ERROR_ZERO_RETURN:
            /* connection was cleanly shut down by peer */
            n = 0;
            break;
        default:
            ereport(COMMERROR,
                    (errcode(ERRCODE_PROTOCOL_VIOLATION),
                     errmsg("unrecognized SSL error code: %d",
                            err)));
            errno = ECONNRESET;
            n = -1;
            break;
    }

    return n;
}

/*
 *    Write data to a secure connection.
 */
ssize_t
be_tls_write(Port *port, void *ptr, size_t len, int *waitfor)
{// #lizard forgives
    ssize_t        n;
    int            err;
    unsigned long ecode;

    errno = 0;
    ERR_clear_error();
    n = SSL_write(port->ssl, ptr, len);
    err = SSL_get_error(port->ssl, n);
    ecode = (err != SSL_ERROR_NONE || n < 0) ? ERR_get_error() : 0;
    switch (err)
    {
        case SSL_ERROR_NONE:
            /* a-ok */
            break;
        case SSL_ERROR_WANT_READ:
            *waitfor = WL_SOCKET_READABLE;
            errno = EWOULDBLOCK;
            n = -1;
            break;
        case SSL_ERROR_WANT_WRITE:
            *waitfor = WL_SOCKET_WRITEABLE;
            errno = EWOULDBLOCK;
            n = -1;
            break;
        case SSL_ERROR_SYSCALL:
            /* leave it to caller to ereport the value of errno */
            if (n != -1)
            {
                errno = ECONNRESET;
                n = -1;
            }
            break;
        case SSL_ERROR_SSL:
            ereport(COMMERROR,
                    (errcode(ERRCODE_PROTOCOL_VIOLATION),
                     errmsg("SSL error: %s", SSLerrmessage(ecode))));
            errno = ECONNRESET;
            n = -1;
            break;
        case SSL_ERROR_ZERO_RETURN:

            /*
             * the SSL connnection was closed, leave it to the caller to
             * ereport it
             */
            errno = ECONNRESET;
            n = -1;
            break;
        default:
            ereport(COMMERROR,
                    (errcode(ERRCODE_PROTOCOL_VIOLATION),
                     errmsg("unrecognized SSL error code: %d",
                            err)));
            errno = ECONNRESET;
            n = -1;
            break;
    }

    return n;
}

/* ------------------------------------------------------------ */
/*                        Internal functions                        */
/* ------------------------------------------------------------ */

/*
 * Private substitute BIO: this does the sending and receiving using send() and
 * recv() instead. This is so that we can enable and disable interrupts
 * just while calling recv(). We cannot have interrupts occurring while
 * the bulk of openssl runs, because it uses malloc() and possibly other
 * non-reentrant libc facilities. We also need to call send() and recv()
 * directly so it gets passed through the socket/signals layer on Win32.
 *
 * These functions are closely modelled on the standard socket BIO in OpenSSL;
 * see sock_read() and sock_write() in OpenSSL's crypto/bio/bss_sock.c.
 * XXX OpenSSL 1.0.1e considers many more errcodes than just EINTR as reasons
 * to retry; do we need to adopt their logic for that?
 */

#ifndef HAVE_BIO_GET_DATA
#define BIO_get_data(bio) (bio->ptr)
#define BIO_set_data(bio, data) (bio->ptr = data)
#endif

static BIO_METHOD *my_bio_methods = NULL;

static int
my_sock_read(BIO *h, char *buf, int size)
{
    int            res = 0;

    if (buf != NULL)
    {
        res = secure_raw_read(((Port *) BIO_get_data(h)), buf, size);
        BIO_clear_retry_flags(h);
        if (res <= 0)
        {
            /* If we were interrupted, tell caller to retry */
            if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)
            {
                BIO_set_retry_read(h);
            }
        }
    }

    return res;
}

static int
my_sock_write(BIO *h, const char *buf, int size)
{
    int            res = 0;

    res = secure_raw_write(((Port *) BIO_get_data(h)), buf, size);
    BIO_clear_retry_flags(h);
    if (res <= 0)
    {
        /* If we were interrupted, tell caller to retry */
        if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)
        {
            BIO_set_retry_write(h);
        }
    }

    return res;
}

static BIO_METHOD *
my_BIO_s_socket(void)
{// #lizard forgives
    if (!my_bio_methods)
    {
        BIO_METHOD *biom = (BIO_METHOD *) BIO_s_socket();
#ifdef HAVE_BIO_METH_NEW
        int            my_bio_index;

        my_bio_index = BIO_get_new_index();
        if (my_bio_index == -1)
            return NULL;
        my_bio_methods = BIO_meth_new(my_bio_index, "PostgreSQL backend socket");
        if (!my_bio_methods)
            return NULL;
        if (!BIO_meth_set_write(my_bio_methods, my_sock_write) ||
            !BIO_meth_set_read(my_bio_methods, my_sock_read) ||
            !BIO_meth_set_gets(my_bio_methods, BIO_meth_get_gets(biom)) ||
            !BIO_meth_set_puts(my_bio_methods, BIO_meth_get_puts(biom)) ||
            !BIO_meth_set_ctrl(my_bio_methods, BIO_meth_get_ctrl(biom)) ||
            !BIO_meth_set_create(my_bio_methods, BIO_meth_get_create(biom)) ||
            !BIO_meth_set_destroy(my_bio_methods, BIO_meth_get_destroy(biom)) ||
            !BIO_meth_set_callback_ctrl(my_bio_methods, BIO_meth_get_callback_ctrl(biom)))
        {
            BIO_meth_free(my_bio_methods);
            my_bio_methods = NULL;
            return NULL;
        }
#else
        my_bio_methods = malloc(sizeof(BIO_METHOD));
        if (!my_bio_methods)
            return NULL;
        memcpy(my_bio_methods, biom, sizeof(BIO_METHOD));
        my_bio_methods->bread = my_sock_read;
        my_bio_methods->bwrite = my_sock_write;
#endif
    }
    return my_bio_methods;
}

/* This should exactly match openssl's SSL_set_fd except for using my BIO */
static int
my_SSL_set_fd(Port *port, int fd)
{
    int            ret = 0;
    BIO           *bio;
    BIO_METHOD *bio_method;

    bio_method = my_BIO_s_socket();
    if (bio_method == NULL)
    {
        SSLerr(SSL_F_SSL_SET_FD, ERR_R_BUF_LIB);
        goto err;
    }
    bio = BIO_new(bio_method);

    if (bio == NULL)
    {
        SSLerr(SSL_F_SSL_SET_FD, ERR_R_BUF_LIB);
        goto err;
    }
    BIO_set_data(bio, port);

    BIO_set_fd(bio, fd, BIO_NOCLOSE);
    SSL_set_bio(port->ssl, bio, bio);
    ret = 1;
err:
    return ret;
}

/*
 *    Load precomputed DH parameters.
 *
 *    To prevent "downgrade" attacks, we perform a number of checks
 *    to verify that the DBA-generated DH parameters file contains
 *    what we expect it to contain.
 */
static DH  *
load_dh_file(char *filename, bool isServerStart)
{// #lizard forgives
    FILE       *fp;
    DH           *dh = NULL;
    int            codes;

    /* attempt to open file.  It's not an error if it doesn't exist. */
    if ((fp = AllocateFile(filename, "r")) == NULL)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode_for_file_access(),
                 errmsg("could not open DH parameters file \"%s\": %m",
                        filename)));
        return NULL;
    }

    dh = PEM_read_DHparams(fp, NULL, NULL, NULL);
    FreeFile(fp);

    if (dh == NULL)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("could not load DH parameters file: %s",
                        SSLerrmessage(ERR_get_error()))));
        return NULL;
    }

    /* make sure the DH parameters are usable */
    if (DH_check(dh, &codes) == 0)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("invalid DH parameters: %s",
                        SSLerrmessage(ERR_get_error()))));
        return NULL;
    }
    if (codes & DH_CHECK_P_NOT_PRIME)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("invalid DH parameters: p is not prime")));
        return NULL;
    }
    if ((codes & DH_NOT_SUITABLE_GENERATOR) &&
        (codes & DH_CHECK_P_NOT_SAFE_PRIME))
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("invalid DH parameters: neither suitable generator or safe prime")));
        return NULL;
    }

    return dh;
}

/*
 *    Load hardcoded DH parameters.
 *
 *    To prevent problems if the DH parameters files don't even
 *    exist, we can load DH parameters hardcoded into this file.
 */
static DH  *
load_dh_buffer(const char *buffer, size_t len)
{
    BIO           *bio;
    DH           *dh = NULL;

    bio = BIO_new_mem_buf((char *) buffer, len);
    if (bio == NULL)
        return NULL;
    dh = PEM_read_bio_DHparams(bio, NULL, NULL, NULL);
    if (dh == NULL)
        ereport(DEBUG2,
                (errmsg_internal("DH load buffer: %s",
                                 SSLerrmessage(ERR_get_error()))));
    BIO_free(bio);

    return dh;
}

/*
 *    Passphrase collection callback
 *
 * If OpenSSL is told to use a passphrase-protected server key, by default
 * it will issue a prompt on /dev/tty and try to read a key from there.
 * That's no good during a postmaster SIGHUP cycle, not to mention SSL context
 * reload in an EXEC_BACKEND postmaster child.  So override it with this dummy
 * function that just returns an empty passphrase, guaranteeing failure.
 */
static int
ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata)
{
    /* Set flag to change the error message we'll report */
    ssl_passwd_cb_called = true;
    /* And return empty string */
    Assert(size > 0);
    buf[0] = '\0';
    return 0;
}

/*
 *    Certificate verification callback
 *
 *    This callback allows us to log intermediate problems during
 *    verification, but for now we'll see if the final error message
 *    contains enough information.
 *
 *    This callback also allows us to override the default acceptance
 *    criteria (e.g., accepting self-signed or expired certs), but
 *    for now we accept the default checks.
 */
static int
verify_cb(int ok, X509_STORE_CTX *ctx)
{
    return ok;
}

/*
 *    This callback is used to copy SSL information messages
 *    into the PostgreSQL log.
 */
static void
info_cb(const SSL *ssl, int type, int args)
{// #lizard forgives
    switch (type)
    {
        case SSL_CB_HANDSHAKE_START:
            ereport(DEBUG4,
                    (errmsg_internal("SSL: handshake start")));
            break;
        case SSL_CB_HANDSHAKE_DONE:
            ereport(DEBUG4,
                    (errmsg_internal("SSL: handshake done")));
            break;
        case SSL_CB_ACCEPT_LOOP:
            ereport(DEBUG4,
                    (errmsg_internal("SSL: accept loop")));
            break;
        case SSL_CB_ACCEPT_EXIT:
            ereport(DEBUG4,
                    (errmsg_internal("SSL: accept exit (%d)", args)));
            break;
        case SSL_CB_CONNECT_LOOP:
            ereport(DEBUG4,
                    (errmsg_internal("SSL: connect loop")));
            break;
        case SSL_CB_CONNECT_EXIT:
            ereport(DEBUG4,
                    (errmsg_internal("SSL: connect exit (%d)", args)));
            break;
        case SSL_CB_READ_ALERT:
            ereport(DEBUG4,
                    (errmsg_internal("SSL: read alert (0x%04x)", args)));
            break;
        case SSL_CB_WRITE_ALERT:
            ereport(DEBUG4,
                    (errmsg_internal("SSL: write alert (0x%04x)", args)));
            break;
    }
}

/*
 * Set DH parameters for generating ephemeral DH keys.  The
 * DH parameters can take a long time to compute, so they must be
 * precomputed.
 *
 * Since few sites will bother to create a parameter file, we also
 * also provide a fallback to the parameters provided by the
 * OpenSSL project.
 *
 * These values can be static (once loaded or computed) since the
 * OpenSSL library can efficiently generate random keys from the
 * information provided.
 */
static bool
initialize_dh(SSL_CTX *context, bool isServerStart)
{
    DH           *dh = NULL;

    SSL_CTX_set_options(context, SSL_OP_SINGLE_DH_USE);

    if (ssl_dh_params_file[0])
        dh = load_dh_file(ssl_dh_params_file, isServerStart);
    if (!dh)
        dh = load_dh_buffer(file_dh2048, sizeof file_dh2048);
    if (!dh)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 (errmsg("DH: could not load DH parameters"))));
        return false;
    }

    if (SSL_CTX_set_tmp_dh(context, dh) != 1)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 (errmsg("DH: could not set DH parameters: %s",
                         SSLerrmessage(ERR_get_error())))));
        return false;
    }
    return true;
}

/*
 * Set ECDH parameters for generating ephemeral Elliptic Curve DH
 * keys.  This is much simpler than the DH parameters, as we just
 * need to provide the name of the curve to OpenSSL.
 */
static bool
initialize_ecdh(SSL_CTX *context, bool isServerStart)
{
#ifndef OPENSSL_NO_ECDH
    EC_KEY       *ecdh;
    int            nid;

    nid = OBJ_sn2nid(SSLECDHCurve);
    if (!nid)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("ECDH: unrecognized curve name: %s", SSLECDHCurve)));
        return false;
    }

    ecdh = EC_KEY_new_by_curve_name(nid);
    if (!ecdh)
    {
        ereport(isServerStart ? FATAL : LOG,
                (errcode(ERRCODE_CONFIG_FILE_ERROR),
                 errmsg("ECDH: could not create key")));
        return false;
    }

    SSL_CTX_set_options(context, SSL_OP_SINGLE_ECDH_USE);
    SSL_CTX_set_tmp_ecdh(context, ecdh);
    EC_KEY_free(ecdh);
#endif

    return true;
}

/*
 * Obtain reason string for passed SSL errcode
 *
 * ERR_get_error() is used by caller to get errcode to pass here.
 *
 * Some caution is needed here since ERR_reason_error_string will
 * return NULL if it doesn't recognize the error code.  We don't
 * want to return NULL ever.
 */
static const char *
SSLerrmessage(unsigned long ecode)
{
    const char *errreason;
    static char errbuf[32];

    if (ecode == 0)
        return _("no SSL error reported");
    errreason = ERR_reason_error_string(ecode);
    if (errreason != NULL)
        return errreason;
    snprintf(errbuf, sizeof(errbuf), _("SSL error code %lu"), ecode);
    return errbuf;
}

/*
 * Return information about the SSL connection
 */
int
be_tls_get_cipher_bits(Port *port)
{
    int            bits;

    if (port->ssl)
    {
        SSL_get_cipher_bits(port->ssl, &bits);
        return bits;
    }
    else
        return 0;
}

bool
be_tls_get_compression(Port *port)
{
    if (port->ssl)
        return (SSL_get_current_compression(port->ssl) != NULL);
    else
        return false;
}

void
be_tls_get_version(Port *port, char *ptr, size_t len)
{
    if (port->ssl)
        strlcpy(ptr, SSL_get_version(port->ssl), len);
    else
        ptr[0] = '\0';
}

void
be_tls_get_cipher(Port *port, char *ptr, size_t len)
{
    if (port->ssl)
        strlcpy(ptr, SSL_get_cipher(port->ssl), len);
    else
        ptr[0] = '\0';
}

void
be_tls_get_peerdn_name(Port *port, char *ptr, size_t len)
{
    if (port->peer)
        strlcpy(ptr, X509_NAME_to_cstring(X509_get_subject_name(port->peer)), len);
    else
        ptr[0] = '\0';
}

/*
 * Convert an X509 subject name to a cstring.
 *
 */
static char *
X509_NAME_to_cstring(X509_NAME *name)
{
    BIO           *membuf = BIO_new(BIO_s_mem());
    int            i,
                nid,
                count = X509_NAME_entry_count(name);
    X509_NAME_ENTRY *e;
    ASN1_STRING *v;
    const char *field_name;
    size_t        size;
    char        nullterm;
    char       *sp;
    char       *dp;
    char       *result;

    (void) BIO_set_close(membuf, BIO_CLOSE);
    for (i = 0; i < count; i++)
    {
        e = X509_NAME_get_entry(name, i);
        nid = OBJ_obj2nid(X509_NAME_ENTRY_get_object(e));
        v = X509_NAME_ENTRY_get_data(e);
        field_name = OBJ_nid2sn(nid);
        if (!field_name)
            field_name = OBJ_nid2ln(nid);
        BIO_printf(membuf, "/%s=", field_name);
        ASN1_STRING_print_ex(membuf, v,
                             ((ASN1_STRFLGS_RFC2253 & ~ASN1_STRFLGS_ESC_MSB)
                              | ASN1_STRFLGS_UTF8_CONVERT));
    }

    /* ensure null termination of the BIO's content */
    nullterm = '\0';
    BIO_write(membuf, &nullterm, 1);
    size = BIO_get_mem_data(membuf, &sp);
    dp = pg_any_to_server(sp, size - 1, PG_UTF8);

    result = pstrdup(dp);
    if (dp != sp)
        pfree(dp);
    BIO_free(membuf);

    return result;
}
